// ========================================================================
// Copyright (c) 1996-2009 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
// The Eclipse Public License is available at 
// http://www.eclipse.org/legal/epl-v10.html
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
// You may elect to redistribute this code under either of these licenses. 
// ========================================================================

package org.eclipse.jetty.server.session;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.log.Logger;

/* ------------------------------------------------------------ */
/**
 * An in-memory implementation of SessionManager.
 * <p>
 * This manager supports saving sessions to disk, either periodically or at shutdown. Sessions can also have their content idle saved to disk to reduce the memory overheads of large idle sessions.
 * <p>
 * This manager will create it's own Timer instance to scavenge threads, unless it discovers a shared Timer instance set as the "org.eclipse.jetty.server.session.timer" attribute of the ContextHandler.
 */
public class HashSessionManager extends AbstractSessionManager
{

	final static Logger __log = SessionHandler.__log;

	protected final ConcurrentMap<String, HashedSession> _sessions = new ConcurrentHashMap<String, HashedSession>();
	private static int __id;
	private Timer _timer;
	private boolean _timerStop = false;
	private TimerTask _task;
	int _scavengePeriodMs = 30000;
	int _savePeriodMs = 0; //don't do period saves by default
	int _idleSavePeriodMs = 0; // don't idle save sessions by default.
	private TimerTask _saveTask;
	File _storeDir;
	private boolean _lazyLoad = false;
	private volatile boolean _sessionsLoaded = false;

	/* ------------------------------------------------------------ */
	public HashSessionManager()
	{
		super();
	}

	/* ------------------------------------------------------------ */
	/* (non-Javadoc)
	 * @see org.eclipse.jetty.servlet.AbstractSessionManager#doStart()
	 */
	@Override
	public void doStart() throws Exception
	{
		super.doStart();

		_timerStop = false;
		ServletContext context = ContextHandler.getCurrentContext();
		if (context != null)
			_timer = (Timer)context.getAttribute("org.eclipse.jetty.server.session.timer");
		if (_timer == null)
		{
			_timerStop = true;
			_timer = new Timer("HashSessionScavenger-" + __id++, true);
		}

		setScavengePeriod(getScavengePeriod());

		if (_storeDir != null)
		{
			if (!_storeDir.exists())
				_storeDir.mkdirs();

			if (!_lazyLoad)
				restoreSessions();
		}

		setSavePeriod(getSavePeriod());
	}

	/* ------------------------------------------------------------ */
	/* (non-Javadoc)
	 * @see org.eclipse.jetty.servlet.AbstractSessionManager#doStop()
	 */
	@Override
	public void doStop() throws Exception
	{
		// stop the scavengers
		synchronized (this)
		{
			if (_saveTask != null)
				_saveTask.cancel();
			_saveTask = null;
			if (_task != null)
				_task.cancel();
			_task = null;
			if (_timer != null && _timerStop)
				_timer.cancel();
			_timer = null;
		}

		// This will callback invalidate sessions - where we decide if we will save 
		super.doStop();

		_sessions.clear();

	}

	/* ------------------------------------------------------------ */
	/**
	 * @return the period in seconds at which a check is made for sessions to be invalidated.
	 */
	public int getScavengePeriod()
	{
		return _scavengePeriodMs / 1000;
	}

	/* ------------------------------------------------------------ */
	@Override
	public int getSessions()
	{
		int sessions = super.getSessions();
		if (__log.isDebugEnabled())
		{
			if (_sessions.size() != sessions)
				__log.warn("sessions: " + _sessions.size() + "!=" + sessions);
		}
		return sessions;
	}

	/* ------------------------------------------------------------ */
	/**
	 * @return seconds Idle period after which a session is saved
	 */
	public int getIdleSavePeriod()
	{
		if (_idleSavePeriodMs <= 0)
			return 0;

		return _idleSavePeriodMs / 1000;
	}

	/* ------------------------------------------------------------ */
	/**
	 * Configures the period in seconds after which a session is deemed idle and saved to save on session memory. The session is persisted, the values attribute map is cleared and the session set to idled.
	 * 
	 * @param seconds Idle period after which a session is saved
	 */
	public void setIdleSavePeriod(int seconds)
	{
		_idleSavePeriodMs = seconds * 1000;
	}

	/* ------------------------------------------------------------ */
	@Override
	public void setMaxInactiveInterval(int seconds)
	{
		super.setMaxInactiveInterval(seconds);
		if (_dftMaxIdleSecs > 0 && _scavengePeriodMs > _dftMaxIdleSecs * 1000)
			setScavengePeriod((_dftMaxIdleSecs + 9) / 10);
	}

	/* ------------------------------------------------------------ */
	/**
	 * @param seconds the period is seconds at which sessions are periodically saved to disk
	 */
	public void setSavePeriod(int seconds)
	{
		int period = (seconds * 1000);
		if (period < 0)
			period = 0;
		_savePeriodMs = period;

		if (_timer != null)
		{
			synchronized (this)
			{
				if (_saveTask != null)
					_saveTask.cancel();
				if (_savePeriodMs > 0 && _storeDir != null) //only save if we have a directory configured
				{
					_saveTask = new TimerTask()
					{

						@Override
						public void run()
						{
							try
							{
								saveSessions(true);
							}
							catch (Exception e)
							{
								__log.warn(e);
							}
						}
					};
					_timer.schedule(_saveTask, _savePeriodMs, _savePeriodMs);
				}
			}
		}
	}

	/* ------------------------------------------------------------ */
	/**
	 * @return the period in seconds at which sessions are periodically saved to disk
	 */
	public int getSavePeriod()
	{
		if (_savePeriodMs <= 0)
			return 0;

		return _savePeriodMs / 1000;
	}

	/* ------------------------------------------------------------ */
	/**
	 * @param seconds the period in seconds at which a check is made for sessions to be invalidated.
	 */
	public void setScavengePeriod(int seconds)
	{
		if (seconds == 0)
			seconds = 60;

		int old_period = _scavengePeriodMs;
		int period = seconds * 1000;
		if (period > 60000)
			period = 60000;
		if (period < 1000)
			period = 1000;

		_scavengePeriodMs = period;
		if (_timer != null && (period != old_period || _task == null))
		{
			synchronized (this)
			{
				if (_task != null)
					_task.cancel();
				_task = new TimerTask()
				{

					@Override
					public void run()
					{
						scavenge();
					}
				};
				_timer.schedule(_task, _scavengePeriodMs, _scavengePeriodMs);
			}
		}
	}

	/* -------------------------------------------------------------- */
	/**
	 * Find sessions that have timed out and invalidate them. This runs in the SessionScavenger thread.
	 */
	protected void scavenge()
	{
		//don't attempt to scavenge if we are shutting down
		if (isStopping() || isStopped())
			return;

		Thread thread = Thread.currentThread();
		ClassLoader old_loader = thread.getContextClassLoader();
		try
		{
			if (_loader != null)
				thread.setContextClassLoader(_loader);

			// For each session
			long now = System.currentTimeMillis();
			for (Iterator<HashedSession> i = _sessions.values().iterator(); i.hasNext();)
			{
				HashedSession session = i.next();
				long idleTime = session.getMaxInactiveInterval() * 1000;
				if (idleTime > 0 && session.getAccessed() + idleTime < now)
				{
					// Found a stale session, add it to the list
					session.timeout();
				}
				else if (_idleSavePeriodMs > 0 && session.getAccessed() + _idleSavePeriodMs < now)
				{
					session.idle();
				}
			}
		} catch (Throwable t)
		{
			if (t instanceof ThreadDeath)
				throw ((ThreadDeath)t);
			else
				__log.warn("Problem scavenging sessions", t);
		} finally
		{
			thread.setContextClassLoader(old_loader);
		}
	}

	/* ------------------------------------------------------------ */
	@Override
	protected void addSession(AbstractSession session)
	{
		if (isRunning())
			_sessions.put(session.getClusterId(), (HashedSession)session);
	}

	/* ------------------------------------------------------------ */
	@Override
	public AbstractSession getSession(String idInCluster)
	{
		if (_lazyLoad && !_sessionsLoaded)
		{
			try
			{
				restoreSessions();
			} catch (Exception e)
			{
				__log.warn(e);
			}
		}

		Map<String, HashedSession> sessions = _sessions;
		if (sessions == null)
			return null;

		HashedSession session = sessions.get(idInCluster);

		if (session == null && _lazyLoad)
			session = restoreSession(idInCluster);
		if (session == null)
			return null;

		if (_idleSavePeriodMs != 0)
			session.deIdle();

		return session;
	}

	/* ------------------------------------------------------------ */
	@Override
	protected void invalidateSessions() throws Exception
	{
		// Invalidate all sessions to cause unbind events
		ArrayList<HashedSession> sessions = new ArrayList<HashedSession>(_sessions.values());
		int loop = 100;
		while (sessions.size() > 0 && loop-- > 0)
		{
			// If we are called from doStop
			if (isStopping() && _storeDir != null && _storeDir.exists() && _storeDir.canWrite())
			{
				// Then we only save and remove the session - it is not invalidated.
				for (HashedSession session: sessions)
				{
					session.save(false);
					removeSession(session, false);
				}
			}
			else
			{
				for (HashedSession session: sessions)
					session.invalidate();
			}

			// check that no new sessions were created while we were iterating
			sessions = new ArrayList<HashedSession>(_sessions.values());
		}
	}

	/* ------------------------------------------------------------ */
	@Override
	protected AbstractSession newSession(HttpServletRequest request)
	{
		return new HashedSession(this, request);
	}

	/* ------------------------------------------------------------ */
	protected AbstractSession newSession(long created, long accessed, String clusterId)
	{
		return new HashedSession(this, created, accessed, clusterId);
	}

	/* ------------------------------------------------------------ */
	@Override
	protected boolean removeSession(String clusterId)
	{
		return _sessions.remove(clusterId) != null;
	}

	/* ------------------------------------------------------------ */
	public void setStoreDirectory(File dir)
	{
		_storeDir = dir;
	}

	/* ------------------------------------------------------------ */
	public File getStoreDirectory()
	{
		return _storeDir;
	}

	/* ------------------------------------------------------------ */
	public void setLazyLoad(boolean lazyLoad)
	{
		_lazyLoad = lazyLoad;
	}

	/* ------------------------------------------------------------ */
	public boolean isLazyLoad()
	{
		return _lazyLoad;
	}

	/* ------------------------------------------------------------ */
	public void restoreSessions() throws Exception
	{
		_sessionsLoaded = true;

		if (_storeDir == null || !_storeDir.exists())
		{
			return;
		}

		if (!_storeDir.canRead())
		{
			__log.warn("Unable to restore Sessions: Cannot read from Session storage directory " + _storeDir.getAbsolutePath());
			return;
		}

		String[] files = _storeDir.list();
		for (int i = 0; files != null && i < files.length; i++)
		{
			restoreSession(files[i]);
		}
	}

	/* ------------------------------------------------------------ */
	protected synchronized HashedSession restoreSession(String idInCuster)
	{
		try
		{
			File file = new File(_storeDir, idInCuster);
			if (file.exists())
			{
				FileInputStream in = new FileInputStream(file);
				HashedSession session = restoreSession(in, null);
				in.close();
				addSession(session, false);
				session.didActivate();
				file.delete();
				return session;
			}
		} catch (Exception e)
		{
			__log.warn("Problem restoring session " + idInCuster, e);
		}
		return null;
	}

	/* ------------------------------------------------------------ */
	public void saveSessions(boolean reactivate) throws Exception
	{
		if (_storeDir == null || !_storeDir.exists())
		{
			return;
		}

		if (!_storeDir.canWrite())
		{
			__log.warn("Unable to save Sessions: Session persistence storage directory " + _storeDir.getAbsolutePath() + " is not writeable");
			return;
		}

		for (HashedSession session: _sessions.values())
			session.save(true);
	}

	/* ------------------------------------------------------------ */
	public HashedSession restoreSession(InputStream is, HashedSession session) throws Exception
	{
		/*
		 * Take care of this class's fields first by calling 
		 * defaultReadObject
		 */
		DataInputStream in = new DataInputStream(is);
		String clusterId = in.readUTF();
		in.readUTF(); // nodeId
		long created = in.readLong();
		long accessed = in.readLong();
		int requests = in.readInt();

		if (session == null)
			session = (HashedSession)newSession(created, accessed, clusterId);
		session.setRequests(requests);
		int size = in.readInt();
		if (size > 0)
		{
			ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(in);
			for (int i = 0; i < size; i++)
			{
				String key = ois.readUTF();
				Object value = ois.readObject();
				session.setAttribute(key, value);
			}
			ois.close();
		}
		else
			in.close();
		return session;
	}

	/* ------------------------------------------------------------ */
	/* ------------------------------------------------------------ */
	protected class ClassLoadingObjectInputStream extends ObjectInputStream
	{

		/* ------------------------------------------------------------ */
		public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException
		{
			super(in);
		}

		/* ------------------------------------------------------------ */
		public ClassLoadingObjectInputStream() throws IOException
		{
			super();
		}

		/* ------------------------------------------------------------ */
		@Override
		public Class<?> resolveClass(java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException
		{
			try
			{
				return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader());
			} catch (ClassNotFoundException e)
			{
				return super.resolveClass(cl);
			}
		}
	}
}
